package ec.genetic.impl;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Random;

public class Evolution {

	private final String padding = "0000000000000000000000000000000000000000000000000000000000000000";
	private Chromosome population[];
	private ParentChromosomes[] parents;
	private boolean exit = false;

	// input
	private final double A;
	private final double B;
	private double leftBound;
	private double rightBound;
	private int populationSize;
	private double accuracy;
	private int iterations;
	private double answer;
	private int iteration;

	// output
	private double startFitness;
	private double finishFitness;

	public Evolution(double a, double b, int populationSize) {
		A = a;
		B = b;
		this.populationSize = populationSize;
		population = new Chromosome[populationSize];
		parents = new ParentChromosomes[populationSize];
	}

	public void letEvolutionSolveIt() {

		firstPopulation(leftBound, rightBound);

		startFitness = roundResult(averagePopulationFitness());

		for (int i = 0; i < iterations; i++) {

			chooseParents();

			recombination();

			createNewPopulation();

			if (isExit()) {

				iteration = i;
				exit = true;
				break;
			}
		}

		if (exit == false) {
			iteration = iterations;
			answer = roundResult(bestFitness());
		}

		finishFitness = roundResult(averagePopulationFitness());

	}

	private void firstPopulation(double min, double max) {

		Double temp;
		Random rand = new Random();

		for (int i = 0; i < populationSize; i++) {

			temp = min + (max - min) * rand.nextDouble();
			population[i] = new Chromosome(convertTo64String(temp), i);
			population[i].data = convertTo64String(temp);

		}

		countLivelihoodRates();

	}

	private void createNewPopulation() {
		for (int i = 0; i < populationSize; i++) {
			population[i] = parents[i].child;
		}

		countLivelihoodRates();
	}

	private void recombination() {

		for (int i = 0; i < populationSize; i++) {
			parents[i].onePointCrossover(i);
		}

	}

	/**
	 * Choose the most perspective parents
	 * 
	 * @param doubleVal
	 * @return
	 */
	private void chooseParents() {

		countChromosomesParentProbability();

		Arrays.sort(population);

		int size = 0;
		int first = 0;
		int second = 1;
		while (size < populationSize) {

			parents[size++] = new ParentChromosomes(population[first],
					population[second]);
			if (size < populationSize)
				parents[size++] = new ParentChromosomes(population[second],
						population[first]);
			first++;
			second++;

		}
	}

	private void countChromosomesParentProbability() {

		double probability;

		Random rand = new Random();
		probability = rand.nextDouble();
		for (int i = 0; i < populationSize; i++) {
			population[i].countParentPropability(probability);
		}

	}

	/**
	 * 
	 * @param population
	 * @return likelihood rate to use element for selection
	 */
	private void countLivelihoodRates() {

		for (int i = 0; i < populationSize; i++) {

			population[i].fitness = equantionError(convertToDouble(population[i].data));
		}

		double sumInverseCoeff = sumOfInverseCoeff();
		for (int i = 0; i < populationSize; i++) {
			population[i].livelihoodRate = (1 / population[i].fitness / sumInverseCoeff);

		}

	}

	private String convertTo64String(double doubleVal) {

		String result = padding
				+ Long.toBinaryString(Double.doubleToRawLongBits(doubleVal));

		return result.substring(result.length() - 64, result.length());
	}

	private double convertToDouble(String s) {

		return Double.longBitsToDouble(new BigInteger(s, 2).longValue());
	}

	/**
	 * 1/x1 + 1/x2 + ... + 1/xn
	 */
	private double sumOfInverseCoeff() {

		double sumInverseCoeff = 0;
		for (int i = 0; i < populationSize; i++) {
			sumInverseCoeff += 1 / Math.abs(population[i].fitness);
		}
		return sumInverseCoeff;

	}

	private double equantionError(double x) {

		return Math.abs(this.A * x + this.B);
	}

	/**
	 * Check if we get necessary accuracy
	 * 
	 * @return true - if yes
	 */
	private boolean isExit() {

		for (int i = 0; i < populationSize; i++) {
			if (Math.abs(population[i].fitness) < 0.1) {
				answer = roundResult(convertToDouble(population[i].data));
				return true;
			}
		}
		return false;
	}

	/**
	 * @return average fitness for population
	 */
	private double averagePopulationFitness() {

		double avarageFitness = 0;
		for (int i = 0; i < populationSize; i++) {

			avarageFitness += population[i].fitness;
		}

		avarageFitness = avarageFitness / populationSize;

		return avarageFitness;
	}
	/**
	 * If did not get needed accuracy - return best fitness
	 * @return chromosome with best fitness
	 */
	private double bestFitness(){
		
		double ans = convertToDouble(population[0].data);
		double bestFit = population[0].fitness;
		for (int i = 1; i < populationSize; i++) {
			if (bestFit > population[i].fitness) {
				bestFit = population[i].fitness;
				ans = convertToDouble(population[i].data);
			}
		}
		
		return ans;
	}
	
	private double roundResult(double value){
		
		BigDecimal fd = new BigDecimal(value);
		BigDecimal cutted = fd.setScale(6, RoundingMode.DOWN);
		return cutted.doubleValue();
	}

	/**
	 * 
	 * Getters and setters
	 */

	public int getPopulationSize() {
		return populationSize;
	}

	public void setPopulationSize(int populationSize) {
		this.populationSize = populationSize;
	}

	public int getIterations() {
		return iterations;
	}

	public void setIterations(int iterations) {
		this.iterations = iterations;
	}

	public double getStartFitness() {
		return startFitness;
	}

	public double getFinishFitness() {
		return finishFitness;
	}

	public int getIteration() {
		return iteration;
	}

	public double getAccuracy() {
		return accuracy;
	}

	public void setAccuracy(double accuracy) {
		this.accuracy = accuracy;
	}

	public double getLeftBound() {
		return leftBound;
	}

	public void setLeftBound(double leftBound) {
		this.leftBound = leftBound;
	}

	public double getRightBound() {
		return rightBound;
	}

	public void setRightBound(double rightBound) {
		this.rightBound = rightBound;
	}

	public double getAnswer() {
		return answer;
	}
	
}
